Not all variables are born equal. Certain variables live for the entire life of the application, and others are created and destroyed a thousand times every second. A variable could be visible only from within a procedure or a module or could be in existence only during well-defined time windows over the lifetime of the application. To better define these concepts, I need to introduce two formal definitions.
In Visual Basic jargon, global variables are those variables declared using the Public keyword in BAS modules. Conceptually, these variables are the simplest of the group because they survive for the life of the application and their scope is the entire application. (In other words, they can be read and modified from anywhere in the current program.) The following code snippet shows the declaration of a global variable:
' In a BAS module Public InvoiceCount as Long ' This is a global variable. |
Visual Basic 6 still supports the Global keyword for backward compatibility with Visual Basic 3 and previous versions, but Microsoft doesn't encourage its use.
In general, it's a bad programming practice to use too many global variables. If possible, you should limit yourself to using module-level or local variables because they allow easier code reuse. If your modules and individual routines rely on global variables to communicate with each other, you can't reuse such code without also copying the definitions of the involved global variables. In practice, however, it's often impossible to build a nontrivial application without using global variables, so my suggestion is this: Use them sparingly and choose for them names that make their scope evident (for example, using a g_ or glo prefix). Even more important, add clear comments stating which global variables are used or modified in each procedure:
' NOTE: this procedure depends on the following global variables: ' g_InvoiceCount : number of invoices (read and modified) ' g_UserName : name of current user (read only) Sub CreateNewInvoice() ... End Sub |
An alternative approach, which I often find useful, is to define a special GlobalUDT structure that gathers all the global variables of the application and to declare one single global variable of type GlobalUDT in one BAS module:
' In a BAS module Public Type GlobalUDT InvoiceCount As Long UserName As String .... End Type Public glo As GlobalUDT |
You can access these global variables using a very clear, unambiguous syntax:
' From anywhere in the application glo.InvoiceCount = glo.InvoiceCount + 1 |
This technique has a number of advantages. First, the scope of the variable is evident by its name. Then if you don't remember the name of your variable, you can just type the three characters glo, and then type the dot and let Microsoft IntelliSense show you the list of all the components. In most cases, you just need to type a few characters and let Visual Basic complete the name for you. It's a tremendous time saver. The third advantage is that you can easily save all your global variables to a data file:
' The same routine can save and load global data in GLO. Sub SaveLoadGlobalData(filename As String, Save As Boolean) Dim filenum As Integer, isOpen As Boolean On Error Goto Error_Handler filenum = FreeFile Open filename For Binary As filenum isOpen = True If Save Then Put #filenum, , glo Else Get #filenum, , glo End If Error_Handler: If isOpen Then Close #filenum End Sub |
The beauty of this approach is that you can add and remove global variables—actually, components of the GlobalUDT structure—without modifying the SaveLoadGlobalData routine. (Of course, you can't correctly reload data stored with a different version of GlobalUDT.)
If you declare a variable using a Private or a Dim statement in the declaration section of a module—a standard BAS module, a form module, a class module, and so on—you're creating a private module-level variable. Such variables are visible only from within the module they belong to and can't be accessed from the outside. In general, these variables are useful for sharing data among procedures in the same module:
' In the declarative section of any module Private LoginTime As Date ' A private module-level variable Dim LoginPassword As String ' Another private module-level variable |
You can also use the Public attribute for module-level variables, for all module types except BAS modules. (Public variables in BAS modules are global variables.) In this case, you're creating a strange beast: a Public module-level variable that can be accessed by all procedures in the module to share data and that also can be accessed from outside the module. In this case, however, it's more appropriate to describe such a variable as a property:
' In the declarative section of Form1 module Public CustomerName As String ' A Public property |
You can access a module property as a regular variable from inside the module and as a custom property from the outside:
' From outside Form1 module... Form1.CustomerName = "John Smith" |
The lifetime of a module-level variable coincides with the lifetime of the module itself. Private variables in standard BAS modules live for the entire life of the application, even if they can be accessed only while Visual Basic is executing code in that module. Variables in form and class modules exist only when that module is loaded in memory. In other words, while a form is active (but not necessarily visible to the user) all its variables take some memory, and this memory is released only when the form is completely unloaded from memory. The next time the form is re-created, Visual Basic reallocates memory for all variables and resets them to their default values (0 for numeric values, "" for strings, Nothing for object variables).
Dynamic local variables are defined within a procedure; their scope is the procedure itself, and their lifetime coincides with that of the procedure:
Sub PrintInvoice() Dim text As String ' This is a dynamic local variable. ... End Sub |
Each time the procedure is executed, a local dynamic variable is re-created and initialized to its default value (0, an empty string, or Nothing). When the procedure is exited, the memory on the stack allocated by Visual Basic for the variable is released. Local variables make it possible to reuse code at the procedure level. If a procedure references only its parameters and its local variables (it relies on neither global nor module-level variables), it can be cut from one application and pasted into another without any dependency problem.
Static local variables are a hybrid because they have the scope of local variables and the lifetime of module-level variables. Their value is preserved between calls to the procedure they belong to until their module is unloaded (or until the application ends, as is the case for procedures inside standard BAS modules). These variables are declared inside a procedure using the Static keyword:
Sub PrintInvoice() Static InProgress As Boolean ' This is a Static local variable. ... End Sub |
Alternatively, you can declare the entire procedure to be Static, in which case all variables declared inside it are considered to be Static:
Static Sub PrintInvoice() Dim InProgress As Boolean ' This is a Static local variable. ... End Sub |
Static local variables are similar to private module-level variables, to the extent that you can move a Static declaration from inside a procedure to the declaration section of the module (you only need to convert Static to Dim, because Static isn't allowed outside procedures), and the procedure will continue to work as before. In general, you can't always do the opposite: Changing a module-level variable into a Static procedure-level variable works if that variable is referenced only inside that procedure. In a sense, a Static local variable is a module-level variable that doesn't need to be shared with other procedures. By keeping the variable declaration inside the procedure boundaries, you can reuse the procedure's code more easily.
Static variables are often useful in preventing the procedure from being accidentally reentered. This is frequently necessary for event procedures, as when, for example, you don't want to process user clicks of the same button until the previous click has been served, as shown in the code below.
Private Sub cmdSearch_Click() Static InProgress As Boolean ' Exit if there is a call in progress. If InProgress Then MsgBox "Sorry, try again later": Exit Sub InProgress = True ' Do your search here. ... ' Then reenable calls before exiting. InProgress = False End Sub |